@workerdeck/ui 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +53 -0
  2. package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-B9CHoq8x.d.mts} +158 -21
  3. package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DII9MmQ8.mjs} +4254 -1661
  4. package/build/SessionPanel-DII9MmQ8.mjs.map +1 -0
  5. package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
  6. package/build/format.d.mts +39 -4
  7. package/build/format.mjs +2 -118
  8. package/build/index.d.mts +442 -45
  9. package/build/index.mjs +105 -2
  10. package/build/index.mjs.map +1 -1
  11. package/build/status-Ydzi7n6j.mjs +143 -0
  12. package/build/status-Ydzi7n6j.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -1
  14. package/build/workspace.mjs +108 -5
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +14 -7
  17. package/src/components/agent/Composer.tsx +189 -89
  18. package/src/components/agent/Conversation.tsx +12 -12
  19. package/src/components/agent/FileCard.tsx +0 -26
  20. package/src/components/agent/FileTree.tsx +9 -8
  21. package/src/components/agent/Loader.tsx +22 -72
  22. package/src/components/agent/Message.tsx +11 -46
  23. package/src/components/agent/PermissionPrompt.tsx +0 -92
  24. package/src/components/agent/QuestionPrompt.tsx +0 -122
  25. package/src/components/agent/Reasoning.tsx +5 -19
  26. package/src/components/agent/Response.tsx +1 -132
  27. package/src/components/agent/SessionPanel.tsx +224 -28
  28. package/src/components/agent/SessionWorkspace.tsx +29 -0
  29. package/src/components/agent/StatusBar.tsx +20 -4
  30. package/src/components/agent/ToolCallCard.tsx +17 -111
  31. package/src/components/agent/Transcript.tsx +710 -203
  32. package/src/components/agent/UsageDialog.tsx +20 -106
  33. package/src/components/agent/UsageMeters.tsx +133 -0
  34. package/src/components/agent/pulse.tsx +3 -2
  35. package/src/components/agent/transcript-rows.ts +82 -0
  36. package/src/components/agent/transcript-variant.tsx +29 -51
  37. package/src/components/agent/use-height-epoch.ts +60 -0
  38. package/src/components/agent/use-path-links.ts +147 -0
  39. package/src/components/agent/use-transcript-jumps.ts +190 -0
  40. package/src/components/prompt-area/cursor-helpers.ts +65 -0
  41. package/src/components/prompt-area/use-prompt-area.ts +16 -10
  42. package/src/components/terminal/PermissionPrompt.tsx +119 -0
  43. package/src/components/terminal/QuestionPrompt.tsx +322 -0
  44. package/src/components/terminal/StatusLine.tsx +159 -0
  45. package/src/components/terminal/TerminalTranscript.tsx +147 -0
  46. package/src/components/terminal/affordances.tsx +118 -0
  47. package/src/components/terminal/diff.tsx +130 -0
  48. package/src/components/terminal/height.ts +727 -0
  49. package/src/components/terminal/items.tsx +449 -0
  50. package/src/components/terminal/markdown.tsx +191 -0
  51. package/src/components/terminal/press.tsx +120 -0
  52. package/src/components/terminal/prompt.tsx +343 -0
  53. package/src/components/terminal/result-preview.ts +72 -0
  54. package/src/components/terminal/row.tsx +132 -0
  55. package/src/components/terminal/scrubber.tsx +663 -0
  56. package/src/components/terminal/surface.tsx +80 -0
  57. package/src/components/terminal/tool-run.ts +91 -0
  58. package/src/index.ts +34 -0
  59. package/src/lib/status.ts +59 -3
  60. package/src/lib/tool-icon.ts +14 -0
  61. package/src/styles/terminal.css +1011 -0
  62. package/src/styles/theme.css +41 -0
  63. package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
  64. package/build/format.mjs.map +0 -1
  65. package/src/components/agent/line-prompt.tsx +0 -249
package/README.md CHANGED
@@ -164,6 +164,59 @@ lose by forgetting a second mount isn't one.
164
164
  - **`statusSurface: 'external'` takes the `⋯` menu's only home with it.** Combining it with
165
165
  `panelSurface: 'internal'` needs a *function* `header` to receive the menu, or those panels
166
166
  become unreachable.
167
+ - **The terminal theme (`variant: 'terminal'`) is a renderer, not a second set of branches.** It
168
+ draws every row itself from `components/terminal/` and the shell mounts it *instead* of the
169
+ components under `components/agent/`, so nothing in there asks which variant it is in — if it is
170
+ drawing at all, it is drawing cards.
171
+ - **The panel mounts three terminal surfaces, and they must agree.** The transcript, the pending
172
+ prompts and the composer each establish their own cell, because each sits in a different part of
173
+ the flex column. `terminalMetrics` is one prop for exactly that reason: hand two of them
174
+ different numbers and the caret lands on a different column from the text above it.
175
+ - **`transcriptDensity` and `transcriptFont` reach `cards` only.** A terminal has one line height
176
+ and is monospace by construction. Under `terminal` both are inert rather than broken — a host
177
+ offering them as settings should say so, or hide them (the dashboard hides them).
178
+ - **`scrubber` and `stickyPrompt` reach `terminal` only**, and by construction rather than policy.
179
+ Both rest on the theme's premise — one line height and one cell make a row's height computable —
180
+ and under `cards` the flags are inert.
181
+ - **`stickyPrompt` pins the real row, not a copy of it.** The prompt at the top of the scroller is
182
+ the same DOM node the transcript already rendered, with its transform clamped to the scroll
183
+ offset. A duplicate header cannot be made to line up with the rows beneath it, which is why this
184
+ is worth the machinery (`rangeExtractor` to keep it mounted, a manual push-off — `position:
185
+ sticky` does nothing on an absolutely positioned element).
186
+ - **Terminal row heights are computed, and the cache invalidates by object identity.** The height
187
+ calculator keys a `WeakMap<TranscriptItem, …>` per (width, cell) epoch, which works only because
188
+ the react reducer replaces item objects on every mutation. Mutate a `TranscriptItem` in place and
189
+ you will serve a stale height for a row that has changed. The epoch is rebuilt from a
190
+ `ResizeObserver` on the **content** element, not the scroller — the panel can resize without the
191
+ wrap width moving, since the content column caps at 48rem.
192
+ - **An item index is not a virtual row index.** `terminalBlocks` folds consecutive tool calls into
193
+ one row and the catch-up recap splices another, so `scrollToIndex(itemIndex)` lands off by the
194
+ fold on any transcript that has either. Go through `rowIndexForItem`.
195
+ - **A row's rendered strings are also its height.** `height.ts` predicts each row's pixel height
196
+ with no DOM, so anything the terminal renderer *writes* must come from the module both sides
197
+ import — `tool-run.ts` for a folded run's summary line, `result-preview.ts` for a collapsed tool
198
+ result and its `… +N` label. Re-spelling either one in the renderer alone desynchronises
199
+ `estimateSize` and the transcript grows a phantom scrollable tail. `dev/height-audit.ts` is the
200
+ gate; it measures against real browser layout, which no jsdom test can do.
201
+ - **`affordances={false}` must leave a way to scroll.** The scrubber replaces the native scrollbar
202
+ while it is interactive; with affordances off, the marks stay painted but inert *and the native
203
+ scrollbar comes back*. A rail that kept the scrollbar hidden while refusing the pointer would
204
+ strand the reader.
205
+ - **Its two metrics must be whole pixels.** `--term-font-size` and `--term-line` are the character
206
+ cell; a line height of `1.5 x 13px` is 19.5px and puts every other row on a half-pixel, which
207
+ softens the text and shows a seam through the diff bands. Horizontal measures are `ch` and
208
+ vertical measures are whole multiples of `--term-line` — nothing in the theme is a px constant.
209
+ - **`--term-bleed` is a contract, not a decoration.** A full-bleed band (a diff hunk, a user
210
+ prompt, a hover fill) cancels it with matched negative margin and padding, so it must equal the
211
+ scroller's own horizontal padding. `TerminalSurface` sets both; a host that pads the scroller
212
+ itself will see bands stop short.
213
+ - **Line numbers come from the wire and nowhere else.** A diff renders `protocol`'s `FilePatch`,
214
+ whose hunks are the engine's own; this package has never read the file, so a number it computed
215
+ would be authoritative-looking and wrong. A patch whose hunks all start at 0 (an approval, where
216
+ the edit has not happened yet) renders *without* a number column rather than a column of zeroes.
217
+ - **Affordances cost no layout, which is what makes `false` a real option.** The hover fill is a
218
+ background and the copy actions are absolutely-positioned overlays one line tall, so
219
+ `affordances={false}` changes no glyph's position — it is the pure article, not a degraded mode.
167
220
  - **Keep `monaco-editor` unreachable from `src/index.ts`.** Tree-shaking does not cover it: Vite
168
221
  resolves Monaco's worker `new URL(...)`s while *transforming* the module, before shaking, and
169
222
  emits megabytes of worker assets it never retracts. That is what the `/workspace` entry point is
@@ -78,23 +78,85 @@ declare function PermissionModeSelect({
78
78
  className
79
79
  }: PermissionModeSelectProps): _$react.JSX.Element;
80
80
  //#endregion
81
+ //#region src/components/terminal/affordances.d.ts
82
+ /**
83
+ * The things a terminal cannot do, and this can.
84
+ *
85
+ * The theme's whole discipline is that it renders like a CLI — but it is not a
86
+ * CLI, and refusing every affordance a pointer makes possible would be cosplay
87
+ * rather than design. A terminal cannot highlight the row under your cursor and
88
+ * cannot put a copy button on a block of output; a web view can do both for free
89
+ * and they are genuinely useful.
90
+ *
91
+ * The rule that keeps this honest is that **each one costs no layout**. A hover
92
+ * fill is a background. An action button is an overlay at the row's right edge,
93
+ * one line tall, absolutely positioned so it displaces nothing. Turn them all
94
+ * off and every glyph is on exactly the same cell it was on — which is what
95
+ * makes `off` a real option rather than a degraded mode, and why it is the mode
96
+ * a host projecting to a real terminal would choose.
97
+ *
98
+ * Off by nothing in particular: both default **on**, because the surface these
99
+ * render on is a browser and pretending otherwise helps nobody. A host that
100
+ * wants the pure article passes `affordances={false}`.
101
+ */
102
+ type TerminalAffordances = {
103
+ /** Fill the row under the pointer, on anything pressable. */hover?: boolean; /** Reveal a row's actions (copy, and whatever a row adds) on hover or focus. */
104
+ actions?: boolean;
105
+ };
106
+ declare function useAffordances(): Required<TerminalAffordances>;
107
+ /**
108
+ * A block that reveals its actions on hover.
109
+ *
110
+ * Wraps rather than decorates because the actions belong to the *block* — a
111
+ * message is many rows and its copy button belongs at the top right of all of
112
+ * them, not on whichever row the pointer happens to be over.
113
+ */
114
+ declare function WithActions({
115
+ actions,
116
+ children,
117
+ className
118
+ }: {
119
+ actions: ReactNode;
120
+ children: ReactNode;
121
+ className?: string;
122
+ }): _$react.JSX.Element;
123
+ /**
124
+ * Copy, as a character.
125
+ *
126
+ * `⧉` and `✓` rather than an SVG for the same reason the rest of the theme uses
127
+ * glyphs: an icon drawn at some other size sits between the cells everything
128
+ * else is on, and reads as a button borrowed from another application. The tick
129
+ * replaces the glyph in place, so the confirmation costs no width either.
130
+ */
131
+ declare function CopyAction({
132
+ text,
133
+ label
134
+ }: {
135
+ text: string;
136
+ label?: string;
137
+ }): _$react.JSX.Element;
138
+ //#endregion
81
139
  //#region src/components/agent/transcript-variant.d.ts
82
140
  /**
83
141
  * How the transcript draws a turn.
84
142
  *
85
143
  * - `cards` — the chat convention: bubbles, bordered tool cards, generous gaps.
86
144
  * Right for a wide dashboard where the transcript is the page.
87
- * - `lines` — one full-width line item per event, transparent, hover-highlit,
88
- * with a glyph in a fixed left gutter. Right where vertical space is the
89
- * scarce resource (a VS Code dock) and the terminal is the reference UX:
90
- * nothing is boxed, the content and its marker carry the comprehension.
145
+ * - `terminal` — the CLI's own form: every row on a character cell, no boxes
146
+ * anywhere, diffs as full-width bands. Right where the transcript sits beside
147
+ * a terminal (a VS Code dock) and where vertical space is scarce.
148
+ *
149
+ * `terminal` is **not** a second set of branches in the components under
150
+ * `components/agent/`: it is its own renderer (`components/terminal/`) that the
151
+ * shell mounts *instead* of them. So a row component here never asks which
152
+ * variant it is in — if it is drawing at all, it is drawing cards.
91
153
  *
92
- * A context rather than a prop chain because every row component needs it and
93
- * only the transcript root knows it and because `Message`/`ToolCallCard` are
94
- * exported on their own, so an embedder composing them by hand gets the right
95
- * treatment for free.
154
+ * A context rather than a prop chain because the pieces that DO need it sit
155
+ * outside the transcript (the composer, the pending prompts) and because
156
+ * `Message`/`ToolCallCard` are exported on their own, so an embedder composing
157
+ * them by hand gets the right treatment for free.
96
158
  */
97
- type TranscriptVariant = 'cards' | 'lines';
159
+ type TranscriptVariant = 'cards' | 'terminal';
98
160
  declare function TranscriptVariantProvider({
99
161
  value,
100
162
  children
@@ -107,16 +169,17 @@ declare function useTranscriptVariant(): TranscriptVariant;
107
169
  * How much room the transcript gives each row.
108
170
  *
109
171
  * - `comfortable` — a blank line between messages, which is what the Claude Code
110
- * CLI does and what the `lines` variant is trying to read like. The default:
111
- * a transcript is prose before it is a table.
172
+ * CLI does. The default: a transcript is prose before it is a table.
112
173
  * - `compact` — rows tight against each other, for a dock where every line of
113
174
  * vertical space is contested.
114
175
  *
115
176
  * Separate from the variant, and deliberately: they answer different questions.
116
- * The variant decides *how a row is drawn* (boxed or not) and follows from the
117
- * surface; density decides *how much air is around it* and is a preference the
118
- * reader holds. Coupling them would mean a dock could not be roomy and a
119
- * dashboard could not be dense.
177
+ * The variant decides *how a row is drawn* and follows from the surface; density
178
+ * decides *how much air is around it* and is a preference the reader holds.
179
+ *
180
+ * Reaches `cards` only. The terminal theme's spacing is a blank *line*, decided
181
+ * per pair of blocks by `needsBlank` — a terminal has one line height, which is
182
+ * the whole premise — so there is nothing there for this to turn.
120
183
  */
121
184
  type TranscriptDensity = 'comfortable' | 'compact';
122
185
  declare function TranscriptDensityProvider({
@@ -142,6 +205,23 @@ declare function useTranscriptDensity(): TranscriptDensity;
142
205
  type TranscriptFont = 'sans' | 'mono';
143
206
  //#endregion
144
207
  //#region src/components/agent/SessionPanel.d.ts
208
+ /**
209
+ * The character cell the terminal theme draws on, in **whole pixels**.
210
+ *
211
+ * One object rather than two props because the panel mounts three separate
212
+ * `TerminalSurface`s — the transcript, the pending prompts, the composer — and
213
+ * they must agree: a prompt drawn at a different line height from the rows above
214
+ * it is three surfaces on three grids, which is the failure this theme is built
215
+ * to make impossible. Passing one value through one prop is what keeps them from
216
+ * drifting.
217
+ *
218
+ * Absent means the CLI's own 13/18. A host that follows an editor font size
219
+ * (VS Code) hands that down instead.
220
+ */
221
+ type TerminalMetrics = {
222
+ fontSize?: number;
223
+ lineHeight?: number;
224
+ };
145
225
  interface SessionPanelProps {
146
226
  client: WorkerDeckClient;
147
227
  sessionId: string | undefined;
@@ -204,12 +284,55 @@ interface SessionPanelProps {
204
284
  * internal ref, so an inline closure is fine). */
205
285
  onVitals?: (vitals: SessionVitals) => void;
206
286
  /**
207
- * How the transcript draws a turn — `'cards'` (default) or `'lines'`, the
208
- * space-efficient terminal treatment: no boxes, no bubbles, one full-width
209
- * hover-highlit row per event behind a gutter glyph. An embedder in a dock
210
- * (the VS Code panel) wants `'lines'`; a full-width dashboard usually doesn't.
287
+ * How the transcript draws a turn — `'cards'` (default, the chat convention)
288
+ * or `'terminal'`, the CLI's own form: every row on a character cell, no boxes
289
+ * anywhere, diffs as full-width bands. An embedder in a dock (the VS Code
290
+ * panel) wants `'terminal'`; a full-width dashboard may prefer cards.
211
291
  */
212
292
  transcriptVariant?: TranscriptVariant;
293
+ /**
294
+ * Terminal theme only: the pointer affordances a real terminal cannot offer —
295
+ * the hover fill, the hover-revealed copy. `false` for none. None of them
296
+ * costs layout, so turning them off changes no glyph's position. See
297
+ * {@link TerminalAffordances}.
298
+ */
299
+ affordances?: TerminalAffordances | boolean;
300
+ /**
301
+ * Terminal theme only: the character cell, in whole pixels. See
302
+ * {@link TerminalMetrics} — it reaches all three of the panel's terminal
303
+ * surfaces, which is why it is one prop and not two per surface.
304
+ */
305
+ terminalMetrics?: TerminalMetrics;
306
+ /**
307
+ * Terminal theme only: replace the scrollbar with the **overview ruler** — a
308
+ * `2ch` rail of coloured marks in three lanes (what you typed, the answer and
309
+ * its turn end, errors and a waiting approval), which you can hover to peek,
310
+ * click to jump, and drag to scrub.
311
+ *
312
+ * Its premise is the terminal theme's own: one line height and one cell make
313
+ * every row's height derivable, so a mark's position is *computed* rather than
314
+ * guessed from rows that have not mounted. That is why it is not offered under
315
+ * `cards` — there the flag is inert.
316
+ *
317
+ * `false` keeps the native scrollbar. So does `affordances={false}`, which
318
+ * leaves the marks painted but inert rather than removing a reader's only way
319
+ * to scroll.
320
+ */
321
+ scrubber?: boolean;
322
+ /**
323
+ * Bookmarked **item indices**, painted full-width on the rail. Paint only —
324
+ * the panel neither stores bookmarks nor offers a way to set one, because who
325
+ * owns that store is the embedder's question (a private pin belongs with the
326
+ * client's watermarks; a shared one is session metadata on the gateway).
327
+ */
328
+ scrubberMarks?: readonly number[];
329
+ /**
330
+ * Terminal theme only: hold the prompt of the turn you are reading at the top
331
+ * of the transcript, as the Claude Code CLI does. The **real row** is pinned
332
+ * rather than a copy drawn above it, so it lines up with the rows beneath by
333
+ * construction — see `TranscriptRows`.
334
+ */
335
+ stickyPrompt?: boolean;
213
336
  /**
214
337
  * How much air the transcript gives each row — `'comfortable'` (default: a
215
338
  * blank line between messages, as the Claude Code CLI leaves) or `'compact'`
@@ -312,6 +435,14 @@ interface SessionPanelProps {
312
435
  * a wildcard.
313
436
  */
314
437
  toolHost?: UseToolCallHostOptions | false;
438
+ /**
439
+ * Keep this session's transcript warm across remounts (default true), so
440
+ * switching back to a recently viewed session paints instantly and replays
441
+ * only what it missed — `UseClaudeSessionOptions.cacheTranscript`. Set
442
+ * `false` for an embedder whose principal varies on one gateway URL by means
443
+ * the client cannot see.
444
+ */
445
+ cacheTranscript?: boolean;
315
446
  className?: string;
316
447
  }
317
448
  /** What an embedder needs to *change* a session it doesn't own the attach for. */
@@ -384,14 +515,20 @@ declare function SessionPanel({
384
515
  transcriptVariant,
385
516
  transcriptDensity,
386
517
  transcriptFont,
518
+ affordances,
519
+ terminalMetrics,
520
+ scrubber,
521
+ scrubberMarks,
522
+ stickyPrompt,
387
523
  controlsSurface,
388
524
  onControls,
389
525
  focusComposerOnClick,
390
526
  unseen,
391
527
  readOnly,
392
528
  toolHost,
529
+ cacheTranscript,
393
530
  className
394
531
  }: SessionPanelProps): _$react.JSX.Element;
395
532
  //#endregion
396
- export { PermissionModeSelectProps as _, SessionVitals as a, TranscriptFont as c, useTranscriptDensity as d, useTranscriptVariant as f, PermissionModeSelect as g, PermissionModeMeta as h, SessionSurfacePanel as i, TranscriptVariant as l, PermissionModeChoice as m, SessionPanel as n, TranscriptDensity as o, PERMISSION_MODES as p, SessionPanelProps as r, TranscriptDensityProvider as s, SessionControls as t, TranscriptVariantProvider as u, permissionModeChoices as v, permissionModeMeta as y };
397
- //# sourceMappingURL=SessionPanel-J2U8v88q.d.mts.map
533
+ export { permissionModeChoices as C, PermissionModeSelectProps as S, useAffordances as _, SessionVitals as a, PermissionModeMeta as b, TranscriptDensityProvider as c, TranscriptVariantProvider as d, useTranscriptDensity as f, WithActions as g, TerminalAffordances as h, SessionSurfacePanel as i, TranscriptFont as l, CopyAction as m, SessionPanel as n, TerminalMetrics as o, useTranscriptVariant as p, SessionPanelProps as r, TranscriptDensity as s, SessionControls as t, TranscriptVariant as u, PERMISSION_MODES as v, permissionModeMeta as w, PermissionModeSelect as x, PermissionModeChoice as y };
534
+ //# sourceMappingURL=SessionPanel-B9CHoq8x.d.mts.map